// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Web.Caching;
using System.Web.Hosting;
using System.Xml.Serialization;
using BlueOnionSoftware.Bloget.Properties;
using BlueOnionSoftware.Bloget.Providers;

namespace BlueOnionSoftware.Bloget
{
    [XmlRoot(ElementName = "Blog")]
    public class Blog
    {
        // Basic Info
        private string _title = Resources.BlogetTitle;
        private string _description = Resources.BlogDescription;
        private string _link = Resources.BlogLink;

        // Syndication
        private string _language = Resources.BlogLanguage;
        private string _copyright = Resources.BlogCopyright;
        private string _webmaster = Resources.BlogWebmaster;
        private string _rssChannelImage = string.Empty;

        private string _rssFooter = Resources.BlogRssFooter;

        private int _rssEntriesPerFeed = 10;

        // Comment related
        private bool _allowComments = true;
        private int _expireComments = 20;
        private int _maximumCommentsPerPost = 20;

        // Appearance
        private int _postsPerPage = 10;
        private int _timeZone;
        private int _editorWidth = 500;
        private int _editorHeight = 400;

        // Email related
        private bool _emailComments;
        private bool _emailPingbacks;
        private string _emailTo = string.Empty;

        // Services related
        private bool _enableMetaWeblogApi;
        private bool _enableRealSimpleDiscovery;

        // Pingback related
        private bool _enableAutoPingBack;
        private int _expirePingBacks = 20;
        private int _maximumPingsBacksPerPost = 20;

        // Images
        private string _imageFolder = "~/images/";
        private string _imageUrl = string.Empty;

        // State Information (not serialized)
        private int _allPostsCount; // not including drafts

        private PostCollection _posts = new PostCollection();
        private CategoryCollection _categories = new CategoryCollection();

        public Blog()
        {
        }

        internal Blog(Blog blog)
        {
            if (blog == null)
            {
                throw new ArgumentNullException("blog");
            }
            LicenseAgreement = blog.LicenseAgreement;
            _title = blog._title;
            _description = blog._description;
            _link = blog._link;
            _language = blog._language;
            _copyright = blog._copyright;
            _webmaster = blog._webmaster;
            RssChannelImage = blog.RssChannelImage;
            RssFooter = blog.RssFooter;
            RssEntriesPerFeed = blog.RssEntriesPerFeed;
            _allowComments = blog._allowComments;
            _expireComments = blog._expireComments;
            _postsPerPage = blog._postsPerPage;
            _timeZone = blog._timeZone;
            _editorWidth = blog._editorWidth;
            _editorHeight = blog._editorHeight;
            _maximumCommentsPerPost = blog._maximumCommentsPerPost;
            _emailComments = blog._emailComments;
            _emailPingbacks = blog._emailPingbacks;
            _emailTo = blog._emailTo;
            _enableRealSimpleDiscovery = blog._enableRealSimpleDiscovery;
            _enableMetaWeblogApi = blog._enableMetaWeblogApi;
            _enableAutoPingBack = blog._enableAutoPingBack;
            EnablePingBackService = blog.EnablePingBackService;
            _expirePingBacks = blog._expirePingBacks;
            _maximumPingsBacksPerPost = blog._maximumPingsBacksPerPost;
            _imageFolder = blog._imageFolder;
            _imageUrl = blog._imageUrl;
            DataContext = blog.DataContext;
            _posts = new PostCollection(blog._posts);
            _categories = new CategoryCollection(blog._categories);
            _allPostsCount = blog._allPostsCount;
            // don't include search in copy
        }

        internal string DataContext { get; set; }

        [XmlElement("accepted_license"), Description("Value indicating if license agreement has been accepted")]
        public bool LicenseAgreement { get; set; }

        [XmlElement("title")]
        [Description("Title of the blog")]
        public string Title
        {
            get { return _title ?? string.Empty; }
            set { _title = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("description")]
        [Description("Phrase or sentence describing the blog")]
        [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        public string Description
        {
            get { return _description; }
            set { _description = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("link")]
        [Description("The URL to the HTML website")]
        public string Link
        {
            get { return _link ?? string.Empty; }
            set { _link = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("language")]
        [Description("The language of the blog")]
        public string Language
        {
            get { return _language ?? string.Empty; }
            set { _language = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("copyright")]
        [Description("Copyright notice")]
        public string Copyright
        {
            get { return _copyright ?? string.Empty; }
            set { _copyright = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("webmaster")]
        [Description("Email address of the webmaster")]
        [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        public string Webmaster
        {
            get { return _webmaster ?? string.Empty; }
            set { _webmaster = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("rss_image")]
        [Description("Channel image for rss feeds")]
        public string RssChannelImage
        {
            get { return _rssChannelImage ?? string.Empty; }
            set { _rssChannelImage = value ?? string.Empty; }
        }

        [XmlElement("rss_footer")]
        [Description("Footer to append to rss entries")]
        public string RssFooter
        {
            get { return _rssFooter ?? string.Empty; }
            set { _rssFooter = value ?? string.Empty; }
        }

        [XmlElement("rss_entries_per_feed")]
        [Description("Maximum number of entries to post per feed")]
        public int RssEntriesPerFeed
        {
            get { return _rssEntriesPerFeed; }
            set { _rssEntriesPerFeed = value; }
        }

        [XmlElement("posts_per_page")]
        [Description("Number of postings to display on a blog page")]
        public int PostsPerPage
        {
            get { return _postsPerPage; }
            set { _postsPerPage = value; }
        }

        [XmlElement("timezone")]
        [Description("Time zone index of blog")]
        public int TimeZone
        {
            get { return _timeZone; }
            set { _timeZone = value; }
        }

        [XmlElement("editor_width")]
        [Description("Width in pixels of the blog editor control")]
        public int EditorWidth
        {
            get { return _editorWidth; }
            set { _editorWidth = value; }
        }

        [XmlElement("editor_height")]
        [Description("Height in pixels of the blog editor control")]
        public int EditorHeight
        {
            get { return _editorHeight; }
            set { _editorHeight = value; }
        }

        [XmlElement("allow_comments")]
        [Description("Enable comments on blog entries")]
        public bool AllowComments
        {
            get { return _allowComments; }
            set { _allowComments = value; }
        }

        [XmlElement("max_comments_per_post")]
        [Description("Maximum number of comments to allow per post")]
        public int MaximumCommentsPerPost
        {
            get { return _maximumCommentsPerPost; }
            set { _maximumCommentsPerPost = value; }
        }

        [XmlElement("expire_comments")]
        [Description("Number of days to allow comments on a posting. Useful for limiting comment spam")]
        public int ExpireComments
        {
            get { return _expireComments; }
            set { _expireComments = value; }
        }

        [XmlElement("email_comments")]
        [Description("Send email notifications of new comments")]
        public bool EmailComments
        {
            get { return _emailComments; }
            set { _emailComments = value; }
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Pingbacks"), XmlElement("email_pingbacks")]
        [Description("Send email notifications of new pingbacks")]
        public bool EmailPingbacks
        {
            get { return _emailPingbacks; }
            set { _emailPingbacks = value; }
        }

        [XmlElement("email_to")]
        [Description("Email address to send notifications to")]
        [SuppressMessage("Microsoft.Design", "CA1062:ValidateArgumentsOfPublicMethods")]
        public string EmailTo
        {
            get { return _emailTo ?? string.Empty; }
            set { _emailTo = (value ?? string.Empty).Trim(); }
        }

        [XmlElement("image_folder")]
        [Description("Folder name where images are uploaded (actual location on your server, example: '~/Images')")]
        public string ImageFolder
        {
            get { return _imageFolder ?? string.Empty; }
            set { _imageFolder = (value ?? string.Empty).Trim(); }
        }

        [Description("http address of images folder")]
        [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings"), XmlElement("image_url")]
        public string ImageUrl
        {
            get { return _imageUrl ?? string.Empty; }

            set
            {
                _imageUrl = (value ?? string.Empty).Trim();

                if (string.IsNullOrEmpty(_imageUrl) == false && _imageUrl.EndsWith("/", StringComparison.Ordinal) == false)
                {
                    _imageUrl += "/";
                }
            }
        }

        [XmlElement("enable_megaweblogapi")]
        [Description("MetaWeblogApi is a programming interface that allows external programs to get and set the text and attributes of weblog posts")]
        public bool EnableMetaWeblogApi
        {
            get { return _enableMetaWeblogApi; }
            set { _enableMetaWeblogApi = value; }
        }

        [XmlElement("enable_rsd")]
        [Description("Enable Real Simple Discovery protocol")]
        public bool EnableRealSimpleDiscovery
        {
            get { return _enableRealSimpleDiscovery; }
            set { _enableRealSimpleDiscovery = value; }
        }

        [XmlElement("enable_auto_pingback")]
        [Description("Automatically send pingbacks to all entry links when published")]
        public bool EnableAutoPingBack
        {
            get { return _enableAutoPingBack; }
            set { _enableAutoPingBack = value; }
        }

        [XmlElement("enable_pingback_service")]
        [Description("Accept pingbacks from others")]
        public bool EnablePingBackService { get; set; }

        [XmlElement("expire_pingbacks")]
        [Description("Number of days to allow pingbacks on a posting. Useful for limiting pingback spam")]
        public int ExpirePingBacks
        {
            get { return _expirePingBacks; }
            set { _expirePingBacks = value; }
        }

        [XmlElement("maximum_pingbacks_per_post")]
        [Description("Maximum number of pingbacks for a single posting. Useful for limiting pingback spam")]
        public int MaximumPingsBacksPerPost
        {
            get { return _maximumPingsBacksPerPost; }
            set { _maximumPingsBacksPerPost = value; }
        }

        [XmlArrayItem("category", typeof (Category))]
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly"), XmlArray("categories")]
        public CategoryCollection Categories
        {
            get { return _categories; }
            set { _categories = value; }
        }

        [XmlArrayItem("post", typeof (Post))]
        [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly"), XmlArray("posts")]
        public PostCollection Posts
        {
            get { return _posts; }
            set { _posts = value; }
        }

        [XmlIgnore]
        public int AllPostsCount
        {
            get { return _allPostsCount; }
        }

        internal static Blog Load(string path)
        {
            Throw.IfNullOrEmpty(path, "path");
            Blog blog;

            try
            {
                blog = BlogProvider.DefaultProvider.Load(path);
            }

            catch (FileNotFoundException)
            {
                return null;
            }

            if (blog._allPostsCount == 0)
            {
                blog.DataContext = path;
                blog._allPostsCount = 0;

                foreach (var post in blog.Posts)
                {
                    if (post.Draft == false)
                    {
                        blog._allPostsCount += 1;

                        foreach (var tag in post.Tags)
                        {
                            if (blog.Categories.Contains(tag))
                            {
                                blog.Categories[tag].Count += 1;
                            }
                        }
                    }
                }
            }

            return blog;
        }

        internal void Save()
        {
            BlogProvider.DefaultProvider.Save(this, DataContext);
        }

        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
        internal Content LoadContent(Guid id)
        {
            Throw.IfEmpty(id, "id");
            string contentPath = null;
            Content content;

            try
            {
                content = BlogProvider.DefaultProvider.LoadContent(id, DataContext);
            }

            catch (Exception ex)
            {
                content = new Content {Text = "Content \"" + contentPath + "\" " + ex.Message};
            }

            return content;
        }

        internal void SaveContent(Guid id, Content content)
        {
            Throw.IfEmpty(id, "id");
            Throw.IfNull(content, "content");
            content.LastUpdate = DateTime.UtcNow;
            BlogProvider.DefaultProvider.SaveContent(id, content, DataContext);
        }

        internal void Backup(Stream stream)
        {
            Throw.IfNull(stream, "stream");
            BlogProvider.DefaultProvider.Backup(this, stream);
        }

        /// <summary>
        /// Enumerates starting from 'start' to 'count' by category
        /// </summary>
        /// <param name="start">starting index</param>
        /// <param name="length">The length.</param>
        /// <param name="category">null means match any category</param>
        /// <param name="draft">true=return draft maxPosts, false=return published maxPosts</param>
        /// <param name="syndicateOnly">return only maxPosts marked as syndicate</param>
        internal IEnumerable<Post> EnumeratePosts(int start, int length, Category category, bool draft, bool syndicateOnly)
        {
            if (start < 0 || length < 0)
            {
                yield break;
            }

            var count = 0;
            var match = 0;

            foreach (var post in _posts)
            {
                if (syndicateOnly && post.Syndicate == false)
                {
                    continue;
                }

                if (post.Draft == draft && (category == null || post.HasCategory(category)))
                {
                    if (count++ >= start)
                    {
                        yield return post;

                        if (++match >= length)
                        {
                            yield break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Enumerates maxPosts for the given year/month
        /// </summary>
        /// <param name="start">The start.</param>
        /// <param name="length">The length.</param>
        /// <param name="monthYear">Only the year/month are used for comparison</param>
        internal IEnumerable<Post> EnumeratePostsByMonth(int start, int length, DateTime monthYear)
        {
            if (start < 0 || length < 0)
            {
                yield break;
            }

            var year = monthYear.Year;
            var month = monthYear.Month;

            var count = 0;
            var match = 0;

            foreach (var post in _posts)
            {
                if (post.Draft == false && post.Created.Year == year && post.Created.Month == month)
                {
                    if (count++ >= start)
                    {
                        yield return post;

                        if (++match >= length)
                        {
                            yield break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Returns an array of month/year dates contained in the blog. Useful for making "Monthly" 
        /// archive listings
        /// </summary>
        internal ArchiveDate[] ArchiveDates()
        {
            const string cacheKey = "bloget_archive_dates";
            var dates = (ArchiveDate[]) HostingEnvironment.Cache[cacheKey];

            if (dates != null)
            {
                return dates;
            }

            var archiveDates = new ArchiveDateCollection();

            foreach (var post in _posts)
            {
                var monthYear = new DateTime(post.Created.Year, post.Created.Month, 1);

                if (archiveDates.Contains(monthYear) == false)
                {
                    archiveDates.Add(new ArchiveDate(monthYear, 1));
                }

                else
                {
                    archiveDates[monthYear].Count += 1;
                }
            }

            dates = archiveDates.ToArray();
            Array.Sort(dates);
            Array.Reverse(dates);

            using (var cd = new CacheDependency(DataContext))
            {
                HostingEnvironment.Cache.Insert(cacheKey, dates, cd);
            }

            return dates;
        }

        internal static bool RenameCategory(Category category, string name)
        {
            Throw.IfNull(category, "category");
            Throw.IfNullOrEmpty(name, "name");
            return true;
        }
    }
}